﻿using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Xml;

using Gavaghan.Geodesy;
using System.Runtime.InteropServices;
using Ionic.Zip;

namespace LeyCDTMapsInGE
{
  public partial class MainForm : Form
  {
    private ProgressForm progressForm;
    private bool conversionCanceled; // TODO: add thread protection

    public MainForm()
    {
      InitializeComponent();
      conversionCanceled = false;
      updateBtnConvert();
    }

    private void btnConvert_Click(object sender, EventArgs e)
    {
      progressForm = new ProgressForm(this);
      var thread = new Thread(new ParameterizedThreadStart(this.convert));
      thread.Start(progressForm);
      progressForm.ShowDialog();
    }

    public void abort()
    {
      conversionCanceled = true;
    }

    private void convert(object obj)
    {
      conversionCanceled = false;

      ProgressForm form = (ProgressForm) obj;

      var CommentChar = "#";

      var outlineXmlSettings = new XmlWriterSettings();
      outlineXmlSettings.ConformanceLevel = ConformanceLevel.Fragment;
      outlineXmlSettings.Indent = true;

      var outlineSb = new StringBuilder();
      var outlineKmlDoc = XmlWriter.Create(outlineSb, outlineXmlSettings);
      
      var xmlSettings = new XmlWriterSettings();
      xmlSettings.Indent = true;
      var memoryStream = new MemoryStream();

      using (var kmlDoc = XmlWriter.Create(memoryStream, xmlSettings))
      {
        kmlDoc.WriteStartDocument();
        kmlDoc.WriteStartElement("kml", "http://www.opengis.net/kml/2.2");
        kmlDoc.WriteStartElement("Document");
        kmlDoc.WriteElementString("name", "Ley CDT Maps");

        kmlDoc.WriteStartElement("Style");
        kmlDoc.WriteAttributeString("id", "map-box");
        kmlDoc.WriteStartElement("LineStyle");
        kmlDoc.WriteElementString("color", "#ff0000ff");
        kmlDoc.WriteElementString("width", "2");
        kmlDoc.WriteEndElement(); // </LineStyle>

        kmlDoc.WriteStartElement("IconStyle");
        kmlDoc.WriteElementString("scale", "0");
        kmlDoc.WriteEndElement(); // </IconStyle>

        kmlDoc.WriteEndElement(); // </Style>

        kmlDoc.WriteStartElement("Folder");
        kmlDoc.WriteElementString("name", "Maps");
        kmlDoc.WriteElementString("open", "1");

        // Open the maps data file, which holds the basic info about the maps
        var reader = new StreamReader(File.OpenRead(txtMapsDataFile.Text));

        int numLines = 0;
        // Determine the number of lines in the file
        while (!reader.EndOfStream)
        {
          var line = reader.ReadLine();
          if (!line.StartsWith(CommentChar) && line.Length != 0)
          {
            ++numLines;
          }
        }

        form.numMaps(numLines);

        // Move the cursor back to the begining of the stream
        reader.BaseStream.Position = 0;
        reader.DiscardBufferedData();

        string currentDir = "";
        int dirDepth = 0;
        int currentLine = 0;
        int currentMap = 0;
        int NumTokens = 12;

        while(!reader.EndOfStream && !conversionCanceled)
        {
          ++currentLine;
          var line = reader.ReadLine();
          if (line.StartsWith(CommentChar) || line.Length == 0)
          {
            continue;
          }

          var values = line.Split(',');
          if (values.Length != NumTokens)
          {
            MessageBox.Show("Line " + currentLine + " had " + values.Length + " tokens, expected " + NumTokens, "Invalid data file format");
            kmlDoc.Close();
            progressForm.conversionCancled();
            return;
          }

          ++currentMap;

          int index = 0;

          var mapName = values[index++];
          var mapDirectory = values[index++];
          var outputName = values[index++];
          var outputDir = values[index++];
          var overlayName = values[index++];
          var cropStr = values[index++];
          var compassRoseLatitude = Double.Parse(values[index++]);
          var compassRoseLongitude = Double.Parse(values[index++]);
          var compassRosePixelX = Double.Parse(values[index++]);
          var compassRosePixelY = Double.Parse(values[index++]);
          var pixelsPerMile = Double.Parse(values[index++]);
          var rotation = Int32.Parse(values[index++]);

          Debug.Assert(index == NumTokens);
          form.currentMap(mapName, currentMap);

          if (overlayName.Length == 0)
          {
            overlayName = mapName;
          }

          if (outputName.Length == 0)
          {
            outputName = mapName;
          }

          // All output images get saves as gifs, since .NET doesn't make transparent gifs well
          // All the input masks are also PNGs
          // This will fail if the filename contains a '.' None of the Ley maps do though
          if (overlayName != "none")
          {
            overlayName = overlayName.Split('.')[0] + ".png";
          }
          outputName = outputName.Split('.')[0] + ".png";
        
          var mapFullFilename = txtCdtMapsDir.Text + Path.DirectorySeparatorChar + "maps" + Path.DirectorySeparatorChar + mapDirectory + Path.DirectorySeparatorChar + mapName;
          form.appendDetail("Processing Map " + mapFullFilename + '\n');

          var mapBitmap = (Bitmap) System.Drawing.Image.FromFile(mapFullFilename, true);

          RotateFlipType rotateType;
          switch (rotation)
          {
            case 0:
              rotateType = RotateFlipType.RotateNoneFlipNone;
              break;
            case 90:
              rotateType = RotateFlipType.Rotate90FlipNone;
              break;
            case 180:
              rotateType = RotateFlipType.Rotate180FlipNone;
              break;
            case 270:
              rotateType = RotateFlipType.Rotate270FlipNone;
              break;
            default:
              // Invalid rotation type
              MessageBox.Show("Bad value: " + rotation + " on line " + currentLine, "Invalid rotation value");
              progressForm.conversionCancled();
              kmlDoc.Close();
              return;
          }
          form.appendDetail("\trotating...");
          mapBitmap.RotateFlip(rotateType);
 
          var newBitmap = new Bitmap(mapBitmap.Width, mapBitmap.Height);
          var mapGraphic = Graphics.FromImage(newBitmap);
          mapGraphic.DrawImage(mapBitmap, new Rectangle(0, 0, newBitmap.Width, newBitmap.Height), 0, 0, newBitmap.Width, newBitmap.Height, GraphicsUnit.Pixel);

          //
          // Make background transparent
          if (overlayName != "none")
          {
            form.appendDetail("masking...");

            var fullOverlayMapName = txtMapMaskDir.Text + Path.DirectorySeparatorChar + mapDirectory + Path.DirectorySeparatorChar + overlayName;
            var overlayMapImage = new Bitmap(fullOverlayMapName);

            overlayMapImage.RotateFlip(rotateType);
            overlayMapImage.MakeTransparent(Color.Black);
            mapGraphic.DrawImage(overlayMapImage, new Point(0, 0));
            newBitmap.MakeTransparent(Color.Orange);
          }

          //
          // Crop the image, removing all rows/columns that are all transparent
          form.appendDetail("cropping...");

          int maxX, maxY, minX, minY;
          Bitmap croppedBitmap;

          if (cropStr == "auto")
          {
            // Autocrop
            trimImage(newBitmap, out minX, out minY, out maxX, out maxY);
            cropImage(newBitmap, out croppedBitmap, minX, minY, maxX, maxY);
          }
          else if (cropStr.Length == 0)
          {
            // no cropping
            croppedBitmap = newBitmap;
            minX = 1;
            minY = 1;
            maxX = croppedBitmap.Width;
            maxY = croppedBitmap.Height;
          }
          else
          {
            // Manual crop
            var cropVals = cropStr.Split('x');
            if (cropVals.Length != 4)
            {
              MessageBox.Show("Bad crop value on line " + currentLine, "Bad Crop Value");
              kmlDoc.Close();
              progressForm.conversionCancled();
              return;
            }
            minX = Int32.Parse(cropVals[0]);
            minY = Int32.Parse(cropVals[1]);
            maxX = Int32.Parse(cropVals[2]);
            maxY = Int32.Parse(cropVals[3]);
            cropImage(newBitmap, out croppedBitmap, minX, minY, maxX, maxY);
          }

          // Adjust compass rose x/y for cropping, in 0-based values
          compassRosePixelX -= minX - 1;
          compassRosePixelY -= minY - 1;

          //
          // Write the image to disk
          var fullOutputPath = txtOutputDir.Text + Path.DirectorySeparatorChar + mapDirectory;
          if (!Directory.Exists(fullOutputPath))
          {
            Directory.CreateDirectory(fullOutputPath);
          }

          var fullOutputName = fullOutputPath + Path.DirectorySeparatorChar + outputName;
          croppedBitmap.Save(fullOutputName, System.Drawing.Imaging.ImageFormat.Png);
          form.appendDetail("saved." + '\n');

          double imagePixelWidth = croppedBitmap.Width;
          double imagePixelHeight = croppedBitmap.Height;

          mapGraphic.Dispose();

          //
          // Now do some calculations
          // instantiate the calculator
          var geoCalc = new GeodeticCalculator();

          // select a reference elllipsoid
          var reference = Ellipsoid.WGS84;

          // Where the compass rose is physically located in space
          var compassRoseCenter = new GlobalCoordinates(new Angle(compassRoseLatitude), 
                                                        new Angle(compassRoseLongitude));

          Angle northBearing = new Angle(0);
          Angle southBearing = new Angle(180);
          Angle eastBearing = new Angle(90);
          Angle westBearing = new Angle(270);

          const double MetersPerMile = 1609.34;

          double metersToNorthEdge = compassRosePixelY / pixelsPerMile * MetersPerMile;
          double metersToSouthEdge = (imagePixelHeight - compassRosePixelY) / pixelsPerMile * MetersPerMile;
          double metersToEastEdge = (imagePixelWidth - compassRosePixelX) / pixelsPerMile * MetersPerMile;
          double metersToWestEdge = compassRosePixelX / pixelsPerMile * MetersPerMile;

          // find the destination
          GlobalCoordinates northDest = geoCalc.CalculateEndingGlobalCoordinates(reference, compassRoseCenter, northBearing, metersToNorthEdge);
          GlobalCoordinates southDest = geoCalc.CalculateEndingGlobalCoordinates(reference, compassRoseCenter, southBearing, metersToSouthEdge);
          GlobalCoordinates eastDest = geoCalc.CalculateEndingGlobalCoordinates(reference, compassRoseCenter, eastBearing, metersToEastEdge);
          GlobalCoordinates westDest = geoCalc.CalculateEndingGlobalCoordinates(reference, compassRoseCenter, westBearing, metersToWestEdge);

          // Directory
          if (outputDir != currentDir)
          {
            var currentDirs = currentDir.Length == 0 ? new string[]{} : currentDir.Split('\\');
            var nextDirs = outputDir.Split('\\');
            dirDepth = nextDirs.Length;

            // Pop up the directory stack until we reach a common base directory
            int? dirDepthChange = null;

            for (int i = 0; i < Math.Min(nextDirs.Length, currentDirs.Length); ++i)
            {
              if (nextDirs[i] != currentDirs[i])
              {
                dirDepthChange = i;
                break;
              }
            }

            if (dirDepthChange.HasValue)
            {
              for (int i = 0; i < currentDirs.Length - dirDepthChange; ++i)
              {
                kmlDoc.WriteEndElement(); // </Folder>
              }
            }
            if (nextDirs.Length < currentDirs.Length)
            {
              for (int i = 0; i < (dirDepthChange.HasValue ? dirDepthChange.Value : currentDirs.Length) - nextDirs.Length; ++i)
              {
                kmlDoc.WriteEndElement(); // </Folder>
              }
            }

            // Push down the stack, addingnew directories
            for (int i = dirDepthChange.HasValue ? dirDepthChange.Value : currentDirs.Length; i < nextDirs.Length; ++i)
            {
              kmlDoc.WriteStartElement("Folder");
              kmlDoc.WriteElementString("name", nextDirs[i]);
            }
            currentDir = outputDir;
          }

          //
          // Box
          outlineKmlDoc.WriteStartElement("Placemark");
          outlineKmlDoc.WriteElementString("name", outputName.Split('.')[0]); // This would fail if the filename contained a '.'
          outlineKmlDoc.WriteElementString("styleUrl", "map-box");
          outlineKmlDoc.WriteStartElement("MultiGeometry");
          outlineKmlDoc.WriteStartElement("LineString");
          outlineKmlDoc.WriteElementString("coordinates",
            westDest.Longitude.Degrees.ToString() + ',' + northDest.Latitude.Degrees.ToString() + ",0\n" +
            eastDest.Longitude.Degrees.ToString() + ',' + northDest.Latitude.Degrees.ToString() + ",0\n" +
            eastDest.Longitude.Degrees.ToString() + ',' + southDest.Latitude.Degrees.ToString() + ",0\n" +
            westDest.Longitude.Degrees.ToString() + ',' + southDest.Latitude.Degrees.ToString() + ",0\n" +
            westDest.Longitude.Degrees.ToString() + ',' + northDest.Latitude.Degrees.ToString() + ",0\n");
          outlineKmlDoc.WriteEndElement(); // </LineString>

          outlineKmlDoc.WriteStartElement("Point");
          outlineKmlDoc.WriteElementString("coordinates",
            (westDest.Longitude.Degrees + eastDest.Longitude.Degrees) / 2.0 + "," +
            (southDest.Latitude.Degrees + northDest.Latitude.Degrees) / 2.0 + ",0");
          outlineKmlDoc.WriteEndElement(); // </Point>

          outlineKmlDoc.WriteEndElement(); // </MultiGeometry>
          outlineKmlDoc.WriteEndElement(); // </Placemark>

          //
          // Ground overlay
          kmlDoc.WriteStartElement("GroundOverlay");
            kmlDoc.WriteElementString("name", outputName.Split('.')[0]); // This would fail if the filename contained a '.'
              kmlDoc.WriteElementString("visibility", "0"); // default layer off
              kmlDoc.WriteStartElement("Icon");
              kmlDoc.WriteElementString("href", mapDirectory + Path.DirectorySeparatorChar + outputName);
            kmlDoc.WriteEndElement(); // </Icon>
            kmlDoc.WriteStartElement("LatLonBox");
              kmlDoc.WriteElementString("north", northDest.Latitude.Degrees.ToString());
              kmlDoc.WriteElementString("south", southDest.Latitude.Degrees.ToString());
              kmlDoc.WriteElementString("east", eastDest.Longitude.Degrees.ToString());
              kmlDoc.WriteElementString("west", westDest.Longitude.Degrees.ToString());
            kmlDoc.WriteEndElement(); // </LatLonBox>
          kmlDoc.WriteEndElement(); // </GroundOverlay>
        }

        reader.Close();

        if (conversionCanceled)
        {
          form.appendDetail("\nConversion Aborted!");
          form.conversionCancled();
          return;
        }

        // Close any folder tags
        for (int i = 0; i < dirDepth; ++i)
        {
          kmlDoc.WriteEndElement(); // </Folder>
        }

        kmlDoc.WriteEndElement(); // </Folder>

        kmlDoc.WriteStartElement("Folder");
        kmlDoc.WriteElementString("name", "Outlines");
        outlineKmlDoc.Flush();
        kmlDoc.WriteRaw(outlineSb.ToString());
        kmlDoc.WriteEndElement(); // </Foler>

        kmlDoc.WriteEndElement(); // </Document>
        kmlDoc.WriteEndElement(); // </kml>
        kmlDoc.WriteEndDocument();        
      }

      form.appendDetail("Zipping KML file" + '\n');
      memoryStream.Seek(0, SeekOrigin.Begin);

      using (var zip = new ZipFile())
      {
        zip.AddEntry("doc.kml", memoryStream);
        zip.Save(txtOutputDir.Text + Path.DirectorySeparatorChar + "ley-maps.kmz");
      }

      form.appendDetail('\n' + "Conversion complete!");
      form.conversionDone();
    }

    private void cropImage(Bitmap origBitmap_, out Bitmap croppedBitmap_, int minX_, int minY_, int maxX_, int maxY_)
    {
      var croppedRect = new Rectangle(minX_, minY_, maxX_ - minX_ + 1, maxY_ - minY_ + 1);
      croppedBitmap_ = new Bitmap(croppedRect.Width, croppedRect.Height);

      using (var croppedGraphic = Graphics.FromImage(croppedBitmap_))
      {
        croppedGraphic.DrawImage(origBitmap_, new Rectangle(0, 0, croppedBitmap_.Width, croppedBitmap_.Height), croppedRect, GraphicsUnit.Pixel);
      }
    }

    private void trimImage(Bitmap origBitmap_, out int minX_, out int minY_, out int maxX_, out int maxY_)
    {
      int? minX = null;
      int? minY = null;
      int? maxX = null;
      int? maxY = null;

      // Check for rows to remove
      for (int x = 0; x < origBitmap_.Width; x++)
      {
        for (int y = 0; y < origBitmap_.Height; y++)
        {
          Color c = origBitmap_.GetPixel(x, y);
          if (c.A != Color.Transparent.A)
          {
            if (!minX.HasValue || x < minX.Value)
            {
              minX = x;
            }
            if (!maxX.HasValue || x > maxX.Value)
            {
              maxX = x;
            }
            if (!minY.HasValue || y < minY.Value)
            {
              minY = y;
            }
            if (!maxY.HasValue || y > maxY.Value)
            {
              maxY = y;
            }
          }
        }
      }

      if (minX.HasValue || minY.HasValue || maxX.HasValue || maxY.HasValue)
      {
        // Use 1-based values for min/max/x/y
        minX_ = minX.HasValue ? minX.Value + 1: 1;
        minY_ = minY.HasValue ? minY.Value + 1: 1;
        maxX_ = maxX.HasValue ? maxX.Value + 1: origBitmap_.Width;
        maxY_ = maxY.HasValue ? maxY.Value + 1: origBitmap_.Height;
        if (minX_ != 1 || minY_ != 1 || maxX_ != origBitmap_.Width || maxY_ != origBitmap_.Height)
        {
          progressForm.appendDetail("[" + minX_ + ',' + minY_ + ',' + maxX_ + ',' + maxY_ + ']');
        }
      }
      else
      {
        minX_ = 1;
        minY_ = 1;
        maxX_ = origBitmap_.Width;
        maxY_ = origBitmap_.Height;
      }
    }

    private void getDirectory(TextBox textBox, bool newFolderButton)
    {
      folderBrowserDialog.Reset();
      folderBrowserDialog.ShowNewFolderButton = newFolderButton;
      if (textBox.Text.Length > 0)
      {
        folderBrowserDialog.SelectedPath = textBox.Text;
      }

      if (folderBrowserDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
      {
        textBox.Text = folderBrowserDialog.SelectedPath;
      }
    }

    private void btnCdtDirBrowse_Click(object sender, EventArgs e)
    {
      getDirectory(txtCdtMapsDir, false);
    }

    private void btnMapMaskDirBrowse_Click(object sender, EventArgs e)
    {
      getDirectory(txtMapMaskDir, false);
    }

    private void btnOutputDirBrowse_Click(object sender, EventArgs e)
    {
      getDirectory(txtOutputDir, true);
    }

    private void btnMapsDataFileBrowse_Click(object sender, EventArgs e)
    {
      var lastDirCharIndex = txtMapsDataFile.Text.LastIndexOf(Path.DirectorySeparatorChar);
      if (lastDirCharIndex > 0)
      {
        openFileDialog.InitialDirectory = txtMapsDataFile.Text.Substring(0, lastDirCharIndex);
        openFileDialog.FileName = txtMapsDataFile.Text.Substring(lastDirCharIndex + 1);
      }

      if (openFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
      {
        txtMapsDataFile.Text = openFileDialog.FileName;
      }
    }

    private void btnExit_Click(object sender, EventArgs e)
    {
      Application.Exit();
    }

    private void updateBtnConvert()
    {
      btnConvert.Enabled = txtCdtMapsDir.Text.Length > 0 &&
                           txtMapMaskDir.Text.Length > 0 &&
                           txtOutputDir.Text.Length > 0 &&
                           txtMapsDataFile.Text.Length > 0;
    }

    private void txtCdtMapsDir_TextChanged(object sender, EventArgs e)
    {
      updateBtnConvert();
    }

    private void txtMapMaskDir_TextChanged(object sender, EventArgs e)
    {
      updateBtnConvert();
    }

    private void txtOutputDir_TextChanged(object sender, EventArgs e)
    {
      updateBtnConvert();
    }

    private void txtMapsDataFile_TextChanged(object sender, EventArgs e)
    {
      updateBtnConvert();
    }

    private void lblAbout_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
    {
      var url = "http://www.joesdiner.org/programs/ley-cdt-maps-ge/";

      try
      {
        System.Diagnostics.Process.Start(url);
      }
      catch (Exception)
      {
        MessageBox.Show("Error opening link. Please manually visit " + url);
      }
    }
  }
}
